//
// Created by Soar Qin on 2017/8/18.
// Copyright (c) 2017 Soar Qin. All rights reserved.
//

#include "common.hh"

#include "http_req.hh"

#include "timer.hh"
#include "loop.hh"

#include "core/async/private/io_event_private.hh"
#include "private/loop_private.hh"

namespace core::async {

struct HttpReqContext {
    CURL *easy = nullptr;
    Loop *loop = nullptr;
    char error[CURL_ERROR_SIZE] = {};
    std::string url;
};

/* Check for completed transfers, and remove their easy handles */
static void checkCurlMulti(LoopContext *c) {
    CURLMsg *msg;
    int msgs_left;

    while((msg = curl_multi_info_read(c->httpMulti, &msgs_left)) != nullptr) {
        if(msg->msg == CURLMSG_DONE) {
            CURL *easy = msg->easy_handle;
            CURLcode res = msg->data.result;
            HttpReqBase *req;
            const char *eff_url;
            curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
            curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &eff_url);
            req->onFinished(eff_url);
            req->stop();
        }
    }
}

class CurlMultiTimer: public Timer {
public:
    using Timer::Timer;

protected:
    void onTimer() override {
        auto *context = loop_.getContext();
        CURLMcode rc;
        int still_running;
        rc = curl_multi_socket_action(context->httpMulti,
                                      CURL_SOCKET_TIMEOUT, 0, &still_running);
        checkCurlMulti(context);
    }
};

class IoEventHttpReq: public IoEvent {
public:
    using IoEvent::IoEvent;

    inline bool start(int fd, short actions) {
        return startInternal(fd, actions);
    }
    inline void enable(short actions) {
        enableInternal(actions);
    }

protected:
    void onEvent(short events) override {
        auto *c = loop_.getContext();
        CURLMcode rc;

        int action =
                ((events & EV_READ) != 0 ? CURL_CSELECT_IN : 0) |
                ((events & EV_WRITE) != 0 ? CURL_CSELECT_OUT : 0);

        int still_running;
        rc = curl_multi_socket_action(c->httpMulti, (curl_socket_t)context_->fd, action, &still_running);
        if (rc != CURLM_OK) return;

        checkCurlMulti(c);
        if(still_running <= 0) {
            if (c->httpTimer != nullptr) {
                delete c->httpTimer;
                c->httpTimer = nullptr;
            }
        }
    }
};

static int curlSockCB(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp) {
    auto *req = (HttpReqBase*) cbp;
    auto *reqev = (IoEventHttpReq*) sockp;
    const char *whatstr[]={ "none", "IN", "OUT", "INOUT", "REMOVE" };

    if(what == CURL_POLL_REMOVE) {
        delete reqev;
        return 0;
    }
    short kind = ((what&CURL_POLL_IN) != 0 ? EV_READ : 0) | ((what&CURL_POLL_OUT) != 0 ? EV_WRITE : 0) | EV_PERSIST;
    if(reqev == nullptr) {
        auto *l = req->getContext()->loop;
        reqev = new IoEventHttpReq(*l);
        if (!reqev->start((int) s, kind)) {
            printf("delete\n");
            delete reqev;
            return -1;
        }
        auto *c = l->getContext();
        curl_multi_assign(c->httpMulti, s, reqev);
    } else {
        reqev->enable(kind);
    }
    return 0;
}

static int curlMultiTimerCB(CURLM *multi, long timeout_ms, HttpReqBase *req) {
    auto *c = req->getContext();
    auto *context = c->loop->getContext();
    if (timeout_ms < 0) {
        if (context->httpTimer != nullptr) {
            delete context->httpTimer;
            context->httpTimer = nullptr;
        }
        return 0;
    }
    auto timeout = static_cast<int64_t>(timeout_ms) * 1000ULL;
    if (context->httpTimer != nullptr)
        context->httpTimer->restart(timeout);
    else {
        context->httpTimer = new CurlMultiTimer(*c->loop);
        context->httpTimer->start(timeout);
    }
    return 0;
}

static size_t curlWriteCB(void *ptr, size_t size, size_t nmemb, void *data)  {
    size_t realsize = size * nmemb;
    auto *req = (HttpReqBase*) data;
    req->onData(ptr, realsize);
    return realsize;
}

static int curlProgCB(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ult,
                      curl_off_t uln) {
    auto *req = (HttpReqBase *)p;
    (void)ult;
    (void)uln;
    req->onProgress(dlnow, dltotal, uln, ult);
    return 0;
}


HttpReqBase::HttpReqBase(Loop &loop): context_ (new HttpReqContext)  {
    context_->loop = &loop;
    auto *context = loop.getContext();
    if (context->httpMulti == nullptr) {
        context->httpTimer = nullptr;
        context->httpMulti = curl_multi_init();
        curl_multi_setopt(context->httpMulti, CURLMOPT_SOCKETFUNCTION, curlSockCB);
        curl_multi_setopt(context->httpMulti, CURLMOPT_SOCKETDATA, this);
        curl_multi_setopt(context->httpMulti, CURLMOPT_TIMERFUNCTION, curlMultiTimerCB);
        curl_multi_setopt(context->httpMulti, CURLMOPT_TIMERDATA, this);
    }
}

HttpReqBase::~HttpReqBase() {
    delete context_;
}

bool HttpReqBase::get(const std::string &url) {
    context_->easy = curl_easy_init();
    context_->error[0] = '\0';
    context_->url = url;

    curl_easy_setopt(context_->easy, CURLOPT_URL, context_->url.c_str());
    curl_easy_setopt(context_->easy, CURLOPT_WRITEFUNCTION, curlWriteCB);
    curl_easy_setopt(context_->easy, CURLOPT_WRITEDATA, this);
    curl_easy_setopt(context_->easy, CURLOPT_VERBOSE, 0L);
    curl_easy_setopt(context_->easy, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(context_->easy, CURLOPT_ERRORBUFFER, context_->error);
    curl_easy_setopt(context_->easy, CURLOPT_PRIVATE, this);
    curl_easy_setopt(context_->easy, CURLOPT_NOPROGRESS, 0L);
    curl_easy_setopt(context_->easy, CURLOPT_XFERINFOFUNCTION, curlProgCB);
    curl_easy_setopt(context_->easy, CURLOPT_XFERINFODATA, this);

    auto *context = context_->loop->getContext();
    CURLMcode rc = curl_multi_add_handle(context->httpMulti, context_->easy);
    if (rc == CURLM_OK)
        return true;
    stop();
    return false;
}

void HttpReqBase::stop() {
    auto *context = context_->loop->getContext();
    curl_multi_remove_handle(context->httpMulti, context_->easy);
    curl_easy_cleanup(context_->easy);
    context_->easy = nullptr;
}

void HttpReqSimple::onData(const void *buf, size_t size) {
    data_.append((const char*)buf, size);
}

}
